package org.melanesia.beans.validation;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

import org.melanesia.beans.AccessorFactory;
import org.melanesia.beans.BeanUtils;
import org.melanesia.beans.PropertyAccessor;
import org.melanesia.beans.validation.exceptions.ValidationException;
import org.melanesia.conf.Config;

/**
 * Helper class for bean validation.
 *
 * Melanesia (at least for now) does not engage automatic bean validation.
 * However one can define validation rules on their own using {@link ValidationRules}
 * and verify them using the Melanesia validator.
 *
 * The validation rules can be described in two ways:
 *
 * <ol>
 * <li>Using the {@link org.melanesia.beans.validation.ValidationRules} annotation.</li>
 * <li>Using melanesia-config.xml</li>
 * </ol>
 *
 * In both cases, the following validation can be defined:
 * <ol>
 * <li>"nullable" - property value cannot be null. If the property was set with a null value,
 * and nullable flag is set to false, validation will fail.</li>
 * <li>"validator" - custom validator implementing
 * {@link org.melanesia.beans.validation.Validator} interface can be passed to
 * validate property</li>
 * </ol>
 *
 * @author marcin.kielar
 *
 */
public abstract class BeanValidator {

    /** Property validation cache. */
    public static final Map<Class<?>, Map<String, PropertyValidationData>> VALIDATED_PROPERTY_CACHE =
            new LinkedHashMap<Class<?>, Map<String, PropertyValidationData>>(
                    Config.getReflectionDataCacheSize(), 0.75F, true) {

                /** Serial version uid for serialization. */
                private static final long serialVersionUID = -379043964609076967L;

                @Override
                public boolean removeEldestEntry(final Map.Entry<Class<?>, Map<String, PropertyValidationData>> eldest) {
                    return size() > Config.getReflectionDataCacheSize();
                }
            };

    /**
     * Validates a java bean.
     *
     * This method uses cache stored in the
     * {@link BeanValidator#VALIDATED_PROPERTY_CACHE} map, to quickly determine
     * the properties to validate. Those properties are determined using
     * reflection analysis of the class and configuration file parsing, and
     * could be very aggravating in multiple repeated calls (which is for
     * example a case when we have a list with thousands of objects to validate).
     *
     * @param bean
     *            bean to validate
     * @throws ValidationException
     *             when validation fails for any reason
     */
    public static final void validateBean(final Object bean) throws ValidationException {

        if (!Config.isBeanValidationEnabled()) {
            return;
        }

        Class<?> beanClass = bean.getClass();

        Map<String, PropertyValidationData> validatedProperties = findValidatedProperties(beanClass);

        for (Map.Entry<String, PropertyValidationData> entry : validatedProperties.entrySet()) {

            String propertyPath = entry.getKey();
            PropertyValidationData data = entry.getValue();

            // Check if property is nullable, or populated with a not null value
            if (!data.isNullable()) {
                PropertyAccessor accessor = AccessorFactory.createAccessor(beanClass, propertyPath);
                if (accessor.get(bean) == null) {
                    throw new ValidationException("Property \"" + propertyPath + "\" does not allow nulls.");
                }
            }

            // Check with custom validator
            if (data.getValidator() != null) {
                PropertyAccessor accessor = AccessorFactory.createAccessor(beanClass, propertyPath);
                data.getValidator().validate(bean, propertyPath, accessor.get(bean));
            }

            // Validate inside
            if (data != null) {
                validateBean(data);
            }
        }
    }

    /**
     * Finds properties to validate. This method uses reflection and
     * configuration file to find bean properties (and property paths for
     * composite properties) for validation. Also see description of
     * {@link #validateBean(Object)}.
     *
     * @param beanClass
     *            bean class to analyze
     * @return map of property validation configuration
     *
     * @see #validateBean(Object)
     */
    public static final Map<String, PropertyValidationData> findValidatedProperties(final Class<?> beanClass) {

        Map<String, PropertyValidationData> propertyPaths = VALIDATED_PROPERTY_CACHE.get(beanClass);

        if (propertyPaths == null) {

            propertyPaths = new HashMap<String, PropertyValidationData>();

            Map<String, PropertyValidationData> topLevelProperties = new HashMap<String, PropertyValidationData>();

            // Lookup XML configuration
            Map<String, PropertyValidationData> xmlProperties = Config.getValidatedProperties(beanClass);
            if (xmlProperties != null) {
                topLevelProperties.putAll(xmlProperties);
            }

            // Lookup methods & fields
            Method[] methods = beanClass.getDeclaredMethods();
            Field[] fields = beanClass.getDeclaredFields();

            // Methods first
            for (Method method : methods) {

                if (method.isAnnotationPresent(ValidationRules.class)) {

                    ValidationRules annotation = method.getAnnotation(ValidationRules.class);

                    String methodName = method.getName();
                    String propertyName = null;

                    if (methodName.startsWith("get") || methodName.startsWith("set")) {
                        propertyName = BeanUtils.decapitalize(methodName.substring(3));
                    } else if (methodName.startsWith("is") && BeanUtils.isAssignableFrom(Boolean.class, method.getReturnType())) {
                        propertyName = BeanUtils.decapitalize(methodName.substring(2));
                    }

                    if (propertyName != null) {
                        topLevelProperties.put(propertyName, new PropertyValidationData(annotation));
                    }
                }
            }

            // Then fields
            for (Field field : fields) {
                if (field.isAnnotationPresent(ValidationRules.class)) {

                    ValidationRules annotation = field.getAnnotation(ValidationRules.class);

                    String propertyName = field.getName();
                    topLevelProperties.put(propertyName, new PropertyValidationData(annotation));
                }
            }

            propertyPaths.putAll(topLevelProperties);

//            // Add composite properties
//            for (Map.Entry<String, PropertyValidationData> entry : topLevelProperties.entrySet()) {
//
//                propertyPaths.put(entry.getKey(), entry.getValue());
//
//                PropertyAccessor propertyAccessor = AccessorFactory.createAccessor(beanClass, entry.getKey());
//                Map<String, PropertyValidationData> subPropertyNames = findValidatedProperties(propertyAccessor.getPropertyClass());
//
//                for (Map.Entry<String, PropertyValidationData> subEntry : subPropertyNames.entrySet()) {
//                    propertyPaths.put(entry.getKey() + Config.getCompositePropertySeparator() + subEntry.getKey(), subEntry.getValue());
//                }
//            }

            VALIDATED_PROPERTY_CACHE.put(beanClass, propertyPaths);
        }

        return propertyPaths;
    }
}
